package main;

import backbone.IForceable;
import backbone.IPaintable;
import backbone.IUpdatable;
import entities.LineObject;
import entities.PointObject;
import entities.TemporarySpring;
import force.Force;
import force.GlobalForce;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;


/**
 * Simulates objects moving around in a bounded environment.
 * 
 * @author Robert C. Duvall
 */
public class Simulation {
    private List<IUpdatable> myUpdatables;
    private List<IPaintable> myPaintables;
    private List<IForceable> myForceables;
    private List<GlobalForce> myGlobalForces;
    private List<PointObject> myPointObjects;
    private Canvas myContainer;
    private Dimension mySize;
    private LineObject myTemporarySpring;

    /**
     * Create a Canvas with the given size.
     * 
     * @param container Canvas container object
     */
    public Simulation(Canvas container) {
        myGlobalForces = new ArrayList<GlobalForce>();
        myUpdatables = new ArrayList<IUpdatable>();
        myPaintables = new ArrayList<IPaintable>();
        myForceables = new ArrayList<IForceable>();
        myPointObjects = new ArrayList<PointObject>();
        myContainer = container;
        mySize = container.getSize();
    }

    /**
     * Paint all shapes on the canvas.
     * 
     * @param pen used to paint shape on the screen
     */
    public void paint(Graphics2D pen) {
        for (IPaintable paintable : myPaintables) {
            paintable.paint(pen);
        }
    }

    /**
     * Called by each step of timer, multiple times per second.
     * 
     * This should update the state of the animated shapes by just
     * a little so they appear to move over time.
     * 
     * @param dt very small unit of time that elapsed
     */
    public void update(double dt) {
        for (IUpdatable updatable : myUpdatables) {
            updatable.update(this, dt);
        }
    }

    /**
     * Returns size (in pixels) of the game area.
     */
    public Dimension getSize() {
        return mySize;
    }
    
    /**
     * Decreases the simulation size available by widthAmount 
     * and heightAmount.
     * @param widthAmount 
     * @param heightAmount
     */
    public void decreaseSize(double widthAmount, double heightAmount) {
        mySize.setSize(mySize.getWidth() - widthAmount, mySize.getHeight()
                - heightAmount);
    }

    /**
     * Increases the simulation size available by widthAmount 
     * and heightAmount.
     * @param widthAmount
     * @param heightAmount
     */
    public void increaseSize(double widthAmount, double heightAmount) {
        mySize.setSize(mySize.getWidth() + widthAmount, mySize.getHeight()
                + heightAmount);
    }

    /**
     * Adds an object to the simulation that should
     * be updated at periodic intervals.
     * @param updatable The object that should be updated.
     */
    public void addUpdatable(IUpdatable updatable) {
        myUpdatables.add(updatable);
    }

    /**
     * Adds an object to the simulation that should
     * be painted at periodic intervals.
     * @param updatable The object that should be painted.
     */
    public void addPaintable(IPaintable paintable) {
        myPaintables.add(paintable);
    }
    
    /**
     * 
     * @param globalForce
     */
    public void addGlobalForce(GlobalForce globalForce) {
        myGlobalForces.add(globalForce);
    }
    
    /**
     * 
     * @param forceable
     */
    public void addForceable(IForceable forceable) {
        myForceables.add(forceable);
    }
    
    /**
     * 
     * @return
     */
    public List<GlobalForce> getForces() {
        return myGlobalForces;
    }
    
    /**
     * Resets the simulation.
     */
    public void reset() {
        myUpdatables.clear();
        myPaintables.clear();
        myGlobalForces.clear();
    }

    /**
     * Turns a specified force on or off.
     * @param name The name of the force to be
     * turned on or off.
     */
    public void toggleForce(String name) {
        for (GlobalForce force : getForce(name)) {
            force.toggle();
        }
    }

    /**
     * 
     * @param name The name of the force to be returned
     * @return The force that was requested by the string name.
     */
    public ArrayList<GlobalForce> getForce(String name) {
        ArrayList<GlobalForce> forceList = new ArrayList<GlobalForce>();
        for (GlobalForce force : myGlobalForces) {
            if (force.getName().equals(name)) {
                forceList.add(force);
            }
        }
        return forceList;
    }
    
    /**
     * 
     * @return The point of the last mouse position.
     */
    public Point getLastMousePosition() {
        return myContainer.getLastMousePosition();
    }
    
    /**
     * Returns the Point of the last press
     * @return
     */
    public Point getLastMousePressed() {
        return myContainer.getLastMousePressed();
    }

    /**
     * Returns the closest Point Object in the simulation 
     * to Point2D p. 
     * @param p
     * @return
     */
    public IForceable getClosestPointObject(Point2D p) {
        IForceable closestForceable = null;
        double shortestDistance = myContainer.getHeight()
                + myContainer.getWidth();
        for (IForceable forceable : myForceables) {
            Point2D forceablePosition = forceable.getPosition();
            double distance = Force.distanceBetween(p, forceablePosition);
            if (distance < shortestDistance) {
                shortestDistance = distance;
                closestForceable = forceable;
            }
        }
        return closestForceable;
    }
    

    /**
     * Removes an object from the simulation.
     * @param id The id of the object to be removed.
     */
    public void removePointObject(int id) {
        for (int i = myPointObjects.size() - 1; i >= 0; i--) {
            if (myPointObjects.get(i).match(id)) {
                myPointObjects.remove(i);
                return;
            }
        }
    }

    /**
     * If you can't figure out what this method does,
     * you're a worse coder than I am. That would be sad :(
     * @return
     */
    public List<PointObject> getPointObjects() {
        return myPointObjects;
    }

    /**
     * 
     * @param p The object to be added to the 
     * simulation.
     */
    public void addPointObject(PointObject p) {
        myPointObjects.add(p);
    }

    /**
     * Should not be called if toRemove is not in the
     * simulation.
     * @param toRemove The object to be removed from 
     * the simulation.
     */
    public void removePaintable(IPaintable toRemove) {
        myPaintables.remove(toRemove);        
    }

    /**
     * Should not be called if toRemove is not in the
     * simulation.
     * @param toRemove The object to be removed from 
     * the simulation.
     */
    public void removeUpdatable(IUpdatable toRemove) {
        myUpdatables.remove(toRemove);      
    }

    public void setTemporarySpring(LineObject tempSpring) {
        myTemporarySpring = tempSpring;        
    }
}
